package com.learn.interceptor;

import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.learn.annotation.EnableOperationLog;
import com.learn.entity.OperationLog;
import com.learn.service.IOperationLogAsynTaskService;
import com.learn.service.IOperationLogService;
import com.learn.utils.EOperationLogType;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.parser.CCJSqlParserManager;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.util.TablesNamesFinder;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.type.TypeHandlerRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.io.StringReader;
import java.lang.reflect.Field;
import java.sql.PreparedStatement;
import java.text.DateFormat;
import java.util.*;

/**
 * 记录单表操作日志
 *
 */
@Component
@Intercepts({@Signature(type = ParameterHandler.class, method = "setParameters", args = {PreparedStatement.class})})
public class OperationLogInterceptor implements Interceptor, ApplicationContextAware {

    private static Logger logger = LoggerFactory.getLogger(OperationLogInterceptor.class);

    @Autowired
    HttpServletRequest request;

    //@Autowired
    private IOperationLogService operationLogService;
//    @Autowired(required = false)
    private volatile IOperationLogAsynTaskService operationLogAsynTaskService;
    private ApplicationContext applicationContext;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {

        try {
            if (invocation.getTarget() instanceof ParameterHandler) {
                ParameterHandler parameterHandler = (ParameterHandler) invocation.getTarget();
                //反射获取BoundSql对象、mappedStatement对象
                Class parameterHandlerClass = parameterHandler.getClass();
                Field boundSqlField = parameterHandlerClass.getDeclaredField("boundSql");
                boundSqlField.setAccessible(true);
                BoundSql boundSql = (BoundSql) boundSqlField.get(parameterHandler);
                Field mappedStatementField = parameterHandlerClass.getDeclaredField("mappedStatement");
                mappedStatementField.setAccessible(true);
                MappedStatement mappedStatement = (MappedStatement) mappedStatementField.get(parameterHandler);
                Configuration configuration = mappedStatement.getConfiguration();

                if (mappedStatement.getSqlCommandType().equals(SqlCommandType.SELECT)) {
                    //查询语句不需记录日志，放行不处理
                    return invocation.proceed();
                }

                //标识是否记录日志
                Boolean isSaveLog = false;

                //判断实体类上有EnableOperationLog注解则记录日志，同时获取表名和主键
                String tableName = getTableName(boundSql.getSql());
                String primaryKey = "";
                if (null != tableName) {
                    String entityName = getEntityName(tableName);
                    try {
                        Class<?> entityClass = Class.forName(entityName);
                        if (entityClass.isAnnotationPresent(EnableOperationLog.class)) {
                            isSaveLog = true;
                        }
                        Field[] declaredFields = entityClass.getDeclaredFields();
                        for (Field declaredField : declaredFields) {
                            if (declaredField.isAnnotationPresent(TableId.class)) {
                                primaryKey = declaredField.getAnnotation(TableId.class).value();
                                break;
                            }
                        }
                    } catch (Exception e) {
                        logger.error("OperationLogInterceptor get entityClass failed: " + e);
                    }
                }

                if (isSaveLog) {
                    if (null == operationLogService) {

                        operationLogService = applicationContext.getBean(IOperationLogService.class);
                    }
                    if(null==operationLogAsynTaskService){
                        operationLogAsynTaskService= applicationContext.getBean(IOperationLogAsynTaskService.class);
                        operationLogAsynTaskService.init();
                    }

                    //设置日志通用信息
                    OperationLog operationLog = setOperationLog();
                    if (StringUtils.isBlank(operationLog.getOperateName())) {
                        logger.error("Record operation log failed, token is null");
                        return invocation.proceed();
                    }
                    operationLog.setTabName(tableName);

                    //获取sql语句
                    String sql = showSql(configuration, boundSql);
                    sql = sql.replaceAll("\'", "").replace("\"", "");
                    String splod = sql.toLowerCase().replace("call", "");

                    switch (mappedStatement.getSqlCommandType()) {
                        case INSERT:
                            setInsertLog(operationLog, sql, tableName, primaryKey);
                            break;
                        case UPDATE:
                            setUpdateLog(operationLog, sql, tableName, primaryKey);
                            break;
                        case DELETE:
                            setDeleteLog(operationLog, sql, tableName, primaryKey);
                            break;
                        default:
                            logger.error("Record operation log failed! unknow operate type!");
                            break;
                    }

                    //设置日志国际化并保存到数据库
                    String language = request.getHeader("language");
                    if (StringUtils.isBlank(language)) {
                        language = "zh_CN";
                    }
                    if (StringUtils.isNotBlank(operationLog.getRemark())) {
//                        operationLogService.save(operationLog);
                        operationLogAsynTaskService.add(operationLog);
                        logger.info("{} save operation log: {}", operationLog.getMethodName(), operationLog);
                    }
                }

            }

        } catch (Exception e) {
            //记录日志错误，不影响业务
            logger.error("Record operation log failed! error: " + e);

        } finally {
            return invocation.proceed();
        }
    }

    private void setInsertLog(OperationLog operationLog, String sql, String tableName, String primaryKey) {
        operationLog.setOperateTypeId(EOperationLogType.ADD.getValue());
        operationLog.setOperateType(EOperationLogType.ADD.getDes());
        //获取新增数据map
        Map<String, Object> columnMap = getColumnMapBySql(sql, tableName);
        String addRemark = getLogRemark(columnMap);
        operationLog.setRemark(addRemark);
        if (columnMap.containsKey(primaryKey)) {
            Object primaryKeyValue = columnMap.get(primaryKey);
            if (null != primaryKey && StringUtils.isNotBlank(primaryKeyValue.toString())) {
                operationLog.setRecordId(primaryKeyValue.toString());
            }
        }
    }

    private void setUpdateLog(OperationLog operationLog, String sql, String tableName, String primaryKey) {
        operationLog.setOperateTypeId(EOperationLogType.UPDATE.getValue());
        operationLog.setOperateType(EOperationLogType.UPDATE.getDes());
        //获取修改操作之前的数据
        String lowerCaseSql = sql.toLowerCase();
        String condition = lowerCaseSql.split("where")[1];
        Map<String, Object> oldDataMap = operationLogService.queryByWhereSql(tableName, condition).get(0);
        Map<String, Object> newDataMap = getColumnMapBySql(sql, tableName);
        List<String> arr = new ArrayList<>();
        String finalPrimaryKey = primaryKey;
        oldDataMap.forEach((k, v) -> {
            if (!k.equals(finalPrimaryKey)) {
                //不比较主键
                String oldResult = getParameterValue(v);
                String newResult = getParameterValue(newDataMap.get(k));
                if(!oldResult.equals(newResult)){
                    arr.add(k + ":" + oldResult + "->" + newResult);
                }
            }
            if (k.equals(finalPrimaryKey)) {
                //设置修改数据主键
                operationLog.setRecordId(v.toString());
            }

        });
        newDataMap.forEach((k, v) -> {
            //可能存在新数据有的列，旧数据没有
            if (!oldDataMap.containsKey(k)) {
                arr.add(k + ":" + " " + "->" + getParameterValue(v));
            }
        });
        String updateRemark = StringUtils.join(arr, ",");
        operationLog.setRemark(updateRemark);
    }

    private void setDeleteLog(OperationLog operationLog, String sql, String tableName, String primaryKey) {
        operationLog.setOperateTypeId(EOperationLogType.DELETE.getValue());
        operationLog.setOperateType(EOperationLogType.DELETE.getDes());
        //获取删除操作之前的数据
        String lowerCaseSql = sql.toLowerCase();
        String deleteCon = lowerCaseSql.split("where")[1];
        List<Map<String, Object>> deleteDataMaps = operationLogService.queryByWhereSql(tableName, deleteCon);
        for (int i = 0; i < deleteDataMaps.size(); i++) {
            Map<String, Object> deleteDataMap = deleteDataMaps.get(i);
            if (i == 0) {
                operationLog.setRemark(getLogRemark(deleteDataMap));
                if (deleteDataMap.containsKey(primaryKey)) {
                    operationLog.setRecordId(deleteDataMap.get(primaryKey).toString());
                }
            } else {
                //针对删除多条记录，其它的在此保存
                OperationLog otherDeleteOperationLog = setOperationLog();
                otherDeleteOperationLog.setOperateTypeId(EOperationLogType.DELETE.getValue());
                otherDeleteOperationLog.setOperateType(EOperationLogType.DELETE.getDes());
                otherDeleteOperationLog.setTabName(tableName);
                otherDeleteOperationLog.setRemark(getLogRemark(deleteDataMap));
                if (deleteDataMap.containsKey(primaryKey)) {
                    otherDeleteOperationLog.setRecordId(deleteDataMap.get(primaryKey).toString());
                }
                String language = request.getHeader("language");
                if (StringUtils.isBlank(language)) {
                    language = "zh_CN";
                }
                if (StringUtils.isNotBlank(otherDeleteOperationLog.getRemark())) {
                    operationLogAsynTaskService.add(otherDeleteOperationLog);
                    logger.info("{} save operation log: {}", otherDeleteOperationLog.getMethodName(), otherDeleteOperationLog);
                }
            }
        }
    }

    /**
     * 通过sql获取表名称
     *
     * @param sql
     * @return java.lang.String
     */
    private String getTableName(String sql) throws JSQLParserException {
        CCJSqlParserManager sqlParserManager = new CCJSqlParserManager();
        TablesNamesFinder tablesNamesFinder = new TablesNamesFinder();
        Statement statement = sqlParserManager.parse(new StringReader(sql));
        List<String> tableNames = tablesNamesFinder.getTableList(statement);
        if (tableNames.size() == 1) {
            //仅记录单表操作日志
            return tableNames.get(0);
        } else {
            return null;
        }
    }

    /**
     * 通过表名称获取实体全名称，用于反射获取实体类
     *
     * @param tableName
     * @return java.lang.String
     */
    private String getEntityName(String tableName) {
        if (StringUtils.isNotBlank(tableName)) {
            /**
             * 这里可以自己优化下
             */
            String prefix = "com.learn.entity.";
            //incident_guide
            String entityName = "";
            String[] split = tableName.split("_");
            for (String s : split) {
                String s1 = s.substring(0, 1);
                String s2 = s.substring(1, s.length());
                entityName = entityName.concat(s1.toUpperCase().concat(s2.toLowerCase()));
            }
            entityName = prefix + entityName;
            return entityName;
        }
        return null;
    }

    /**
     * 获取sql语句中列名和对应值的map
     *
     * @param sql
     * @param tableName
     * @return java.util.Map<java.lang.String,java.lang.Object>
     */
    private Map<String, Object> getColumnMapBySql(String sql, String tableName) {
        HashMap<String, Object> columnMap = new HashMap<>();
        if (StringUtils.isNotBlank(sql) && StringUtils.isNotBlank(tableName)) {
            String lowerCaseSql = sql.toLowerCase();
            if (lowerCaseSql.startsWith("insert")) {
                //insert into table_name (column1,column2,column3,...) values (value1,value2,value3,...);
                String beginStr = "insert into " + tableName + " (";
                int beginIndex = lowerCaseSql.indexOf(beginStr) + beginStr.length();
                int endIndex = lowerCaseSql.indexOf(") values");
                String columnStr = sql.substring(beginIndex, endIndex).trim();
                String[] columnArray = columnStr.split(",");
                String valuesStr = sql.substring(lowerCaseSql.indexOf("values (") + 8, sql.length() - 1).trim();
                String[] valuesArray = valuesStr.split(",");
                for (int i = 0; i < columnArray.length; i++) {
                    String columnName = columnArray[i].trim();
                    String value = valuesArray[i].trim();
                    columnMap.put(columnName, value);
                }

            } else if (lowerCaseSql.startsWith("update")) {
                //update table_name set column1=value1,column2=value2,... where some_column=some_value;
                String beginStr = "update " + tableName + " set ";
                int beginIndex = lowerCaseSql.indexOf(beginStr) + beginStr.length();
                int endIndex = lowerCaseSql.indexOf(" where");
                String newColumnValue = sql.substring(beginIndex, endIndex).trim();
                String[] newColumnValueArray = newColumnValue.split(",");
                for (String s : newColumnValueArray) {
                    String[] keyValueArray = s.trim().split("=");
                    String columnName = keyValueArray[0].trim();
                    String value = " ";
                    if (keyValueArray.length > 1) {
                        value = keyValueArray[1].trim();
                    }
                    columnMap.put(columnName, value);
                }

            } else if (lowerCaseSql.startsWith("DELETE")) {
                //delete from table_name where some_column=some_value;
            }
        }
        return columnMap;
    }

    /**
     * 拼接日志操作内容
     *
     * @param columnMap
     * @return java.lang.String
     */
    private String getLogRemark(Map<String, Object> columnMap) {
        List<String> arr = new ArrayList<>();
        for (String columnName : columnMap.keySet()) {
            Object value = columnMap.get(columnName);
            if (null != value && !"null".equals(value) && !"".equals(value)) {
                arr.add(columnName + ":" + value);
            }
        }
        return StringUtils.join(arr, ",");
    }

    /**
     * 设置日志通用参数
     *
     * @param
     * @return void
     */
    private OperationLog setOperationLog() {
        OperationLog operationLog = new OperationLog();
        Long operationUserId = 123L;
        String operationUserName = "abc";
        String methodName = request.getServletPath().replaceFirst("/", "");
        operationLog.setLogId(IdWorker.getId());
        operationLog.setOperateId(operationUserId);
        operationLog.setOperateName(operationUserName);
        operationLog.setMethodName(methodName);
        operationLog.setOperateTime(new Date());
        return operationLog;
    }

    private String showSql(Configuration configuration, BoundSql boundSql) {
        Object parameterObject = boundSql.getParameterObject();
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        String sql = boundSql.getSql().replaceAll("[\\s]+", " ");
        if (parameterMappings.size() > 0 && parameterObject != null) {
            TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
            if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                sql = sql.replaceFirst("\\?", getParameterValue(parameterObject));
            } else {
                MetaObject metaObject = configuration.newMetaObject(parameterObject);
                for (ParameterMapping parameterMapping : parameterMappings) {
                    String propertyName = parameterMapping.getProperty();
                    if (metaObject.hasGetter(propertyName)) {
                        Object obj = metaObject.getValue(propertyName);
                        sql = sql.replaceFirst("\\?", getParameterValue(obj));
                    } else if (boundSql.hasAdditionalParameter(propertyName)) {
                        Object obj = boundSql.getAdditionalParameter(propertyName);
                        sql = sql.replaceFirst("\\?", getParameterValue(obj));
                    }
                }
            }
        }
        return sql;
    }

    private String getParameterValue(Object obj) {
        String value = null;
        if (obj instanceof String) {
            //value = "'" + obj.toString() + "'";
            value = obj.toString().replaceAll(":", "#####").replaceAll(",", "@@@@@");
        } else if (obj instanceof Date) {
            DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.CHINA);
            //value = "'" + formatter.format(obj) + "'";
            value = formatter.format(obj);
        } else {
            if (obj != null) {
                value = obj.toString();
            } else {
                value = " ";
            }

        }
        return value;
    }

    private Locale getLogLocale(String languageType) {
        switch (languageType) {
            case "zh-CN":
            case "zh_CN":
                return Locale.CHINA;
            case "en_US":
            case "en":
                return Locale.US;
            case "es_BO":
                return new Locale("es", "BO");
            case "ru-RU":
            case "ru_RU":
                return new Locale("ru", "RU");
            case "fr_FR":
            case "fr-FR":
                return new Locale("fr", "FR");
            default:
                return Locale.CHINA;
        }
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {

    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {

        this.applicationContext = applicationContext;
    }
}
